package com.trgis.zhejiangimg;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.net.URIBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.*;
import java.net.URI;
import java.util.*;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;


/**
 * 2024/6/12
 * 主下载类
 * @author fengyu
 */
@Component
public class Download {
    private static final Logger log = LoggerFactory.getLogger(Download.class);

    /**
     * 第0级，最大分辨率，360度除以256像素
     */
    private static final double maxResolution = 360d / 256;

    /**
     * 十进制的经纬度坐标值，限定下载范围
     * 0: 左下角经度 *
     * 1：左下角维度 *
     * 2：右上角经度 *
     * 3：右上角维度 *
     */
    @Value("${bbox}")
    private double[] bbox = { 120.11003191900011, 30.317250201000036,
                                120.34087315100011, 30.52377543800007 };

    @Value("${min_lv}")
    private int minLevel = 11;

    @Value("${max_lv}")
    private int maxLevel = 20;

    @Value("${layer.url}")
    private String layerUrl;
    @Value("${layer.name}")
    private String layerName;

    @Value("${png_dir}")
    private String pngDir;

    @Value("${pool.size}")
    private int poolSize = 10;

    @Resource
    HttpPooling httpPooling;

    @Resource(name = "token")
    public Signature signature;

    private ScheduledExecutorService service = Executors.newScheduledThreadPool(poolSize);

    private final WmtsTileMatrixSet wmtsTileMatrixSet = new WmtsTileMatrixSet();

    private String host;
    private int port;

    /**
     * 用力记录下没获取到的瓦片，依次为层级、行、列号*
     */
    private final List<int[]> tileErrors = new ArrayList<>();

    /**
     * 已下载瓦片计数*
     */
    private int count = 0;
    private Boolean isDownloading = true;

    private void begin() {
        // 检查必要参数
        if (bbox.length == 4) {
            if (bbox[0] < bbox[2] && bbox[2] < 180 && bbox[0] > -180 && bbox[1] < bbox[3] && bbox[1] > -90 && bbox[3] < 90) {
                log.info("{}, {}, {}, {}", bbox[0], bbox[1], bbox[2], bbox[3]);
            } else {
                log.info("坐标值出错，应是左下角坐标和右上角坐标，如：120.12,30.12,120.42,30.81");
                return;
            }
        } else {
            log.info("应给定四个坐标值");
            return;
        }
//        log.info(signature.signatrue());

        String[] strs = layerUrl.replace("http://", "").replace("https://", "").split("/");
        if (strs.length < 2){
            log.info("wmts服务地址错误");
            return;
        }

        String[] str2 = strs[0].split(":");
        host = str2[0];
        port = (str2.length > 1 && str2[1].matches("\\d+")) ? Integer.parseInt(str2[1]) : 80;

        initTileMatrixSet();
        log.info("开始下载");
        TimerTask timerTask = new TimerTask() {
            @Override
            public void run() {
                log.info("已下载瓦片数量：{} , 占比 {}", count, String.format("%.2f%%",(((double)count / wmtsTileMatrixSet.getTileCount())) * 100));
            }
        };
        Timer timer = new Timer();
        timer.schedule(timerTask, 1000, 10000);

        // 开始分层下载
        download(wmtsTileMatrixSet);
        if (tileErrors.size() > 0) {
            log.info("还有 {} 张瓦片缓存失败", tileErrors.size());
            try {
                选择是否重新下载();
            } catch (Exception e) {
                记录下载失败的瓦片();
            }
        }
        log.info("已下载瓦片数量：{} , 占比 {}", count, String.format("%.2f%%",(((double)count / wmtsTileMatrixSet.getTileCount())) * 100));
        timer.cancel();
    }

    /**
     * 从四至范围算每层的行列号范围，计算每层分辨率 *
     * 默认的瓦片起始点是 -180,90 *
     * 即左上角开始以行列号数 *
     */
    private void initTileMatrixSet(){
        for (int i = minLevel; i <= maxLevel ; i++) {
            // 计算每层的分辨率
            double resolution = maxResolution / (1 << i);
            wmtsTileMatrixSet.addResolution(resolution);

            int count = 1 << i;
            double dy = 360d/ count;
            int minCol = (int) ((bbox[0] + 180) / dy);
            int maxCol = (int) ((bbox[2] + 180) / dy);

            int maxRow = count - (int) ((bbox[1] + 180 + 90) / dy) -1;
            int minRow = count - (int) ((bbox[3] + 180 + 90) / dy) -1;

            TileLevelInfo tileLevelInfo = new TileLevelInfo(i, minRow, maxRow, minCol, maxCol);
            log.info("第{}级瓦片总数：{}, 范围col: {} - {}, row: {} - {}", i, tileLevelInfo.getCount(), minCol, maxCol, minRow, maxRow);
            wmtsTileMatrixSet.getMatrixSet().add(tileLevelInfo);
        }
    }

    private void download(WmtsTileMatrixSet wmtsTileMatrixSet){
        //
        for (TileLevelInfo wmtsTileMatrix : wmtsTileMatrixSet.getMatrixSet()) {
            accept(wmtsTileMatrix);
        }
        service.shutdown();
        while (true){
            if (service.isTerminated()) break;
        }
        try {
            FileWriter fileWriter = new FileWriter(pngDir + "/info.txt");
            fileWriter.write("resolutions: " + wmtsTileMatrixSet.getResolutions());
            fileWriter.flush();
            fileWriter.close();
        } catch (IOException e) {
            log.info("resolutions: {}", wmtsTileMatrixSet.getResolutions());
        }
    }

    private synchronized HttpGet createQueryURI(final String z, final String row, final String col) throws Exception {
        String str = signature.signatrue();
        URI uri = new URIBuilder(layerUrl + "?" + str)
                .addParameter("layer", layerName)
                .addParameter("style", "default")
                .addParameter("tilematrixset", "esritilematirx")
                .addParameter("Service", "WMTS")
                .addParameter("Request", "GetTile")
                .addParameter("TileMatrix", z)
                .addParameter("TileCol", col)
                .addParameter("TileRow", row)
                .addParameter("Format", "image/png")
                .build();

        return new HttpGet(uri);
    }

    private Runnable getPng(final HttpGet httpGet, final String filePath, final int[] ts) {
        return () -> {
            int result;
            try {
                result = httpPooling.getHttpClient(host, port).execute(httpGet, response -> {
                    if (response.getCode() == HttpStatus.SC_OK) {
                        InputStream inputStream = response.getEntity().getContent();
                        BufferedImage imageIO = ImageIO.read(inputStream);
                        ImageIO.write(imageIO, "png", new File(filePath));
                        inputStream.close();
                        count++;
                        return 0;
                    } else {
                        return -1;
                    }
                });
            } catch (IOException e) {
                result = -1;
            }
            if (result < 0){
                log.error("未获取瓦片 z：{}，row:{}, col:{}", ts[0], ts[1], ts[2]);
                tileErrors.add(ts);
            }
        };
    }

    private void accept(TileLevelInfo matrix) {
        int level = matrix.getLevel();

        for (int i = matrix.getMinRow(); i <= matrix.getMaxRow(); i++) {
            File dir = new File(pngDir + "/" + level + "/" + i);
            if (!dir.isDirectory()) {
                if (!dir.mkdirs()){
                    log.error("创建目录不成功：{},跳过本行瓦片", dir.getPath());
                    continue;
                }
            }
            for (int j = matrix.getMinCol(); j <= matrix.getMaxCol(); j++) {
                int[] ts = {level, i, j};
                HttpGet httpGet = null;
                try {
                    httpGet = createQueryURI(String.valueOf(level), String.valueOf(i), String.valueOf(j));
                } catch (Exception e) {
                    log.error("未获取瓦片 z：{}，row:{}, col:{}", level, i, j);
                    tileErrors.add(ts);
                }
                service.execute(getPng(httpGet, dir.getPath() + "/" + j + ".png", ts));
            }
        }
    }

    public void 选择是否重新下载() throws Exception {
        System.out.println("再尝试一次下载失败的瓦片，请输入：y，放弃下载请输入：q 或 n");
        ((CommandLineRunner) args -> {
            Scanner scanner = new Scanner(System.in);
            String in = scanner.nextLine();
            boolean b = true;
            while (b) {
                switch (in) {
                    case "y" -> {
                        b = false;
                        失败瓦片尝试重新下载();
                    }
                    case "q", "n" -> {
                        b = false;
                        记录下载失败的瓦片();
                    }
                    default -> {
                        System.out.println("输入y 或者 q n");
                        in = scanner.nextLine();
                    }
                }
            }
        }).run("");
    }

    private void 失败瓦片尝试重新下载() throws Exception {
        service = Executors.newScheduledThreadPool(poolSize);
        for (Iterator<int[]> it = tileErrors.iterator(); it.hasNext(); ) {
            int[] ts = it.next();
            HttpGet httpGet;
            try {
                httpGet = createQueryURI(String.valueOf(ts[0]), String.valueOf(ts[1]), String.valueOf(ts[2]));
            } catch (Exception e) {
                continue;
            }
            it.remove();
            service.execute(getPng(httpGet, pngDir + "/"+ts[0] + "/" + ts[1] + "/" + ts[2] + ".png", ts));
        }
        service.shutdown();
        while (true){
            if (service.isTerminated()) break;
        }
        if (tileErrors.size() > 0) {
            log.info("还有 {} 张瓦片缓存失败", tileErrors.size());
            选择是否重新下载();
        }
    }

    private void 记录下载失败的瓦片() {
        try {
            BufferedWriter bw = new BufferedWriter(new FileWriter(pngDir + "/lose-tiles.txt"));
            bw.append("z , row , col");
            bw.newLine();
            log.info("有 {} 张瓦片下载失败，记录在lose-tiles.txt文件中", tileErrors.size());
            for (Iterator<int[]> it = tileErrors.iterator(); it.hasNext(); it.remove()) {
                int[] ts = it.next();
                bw.append(String.valueOf(ts[0])).append(" , ")
                        .append(String.valueOf(ts[1])).append(" , ")
                        .append(String.valueOf(ts[2]));
                bw.newLine();
            }
            bw.flush();
            bw.close();
        } catch (IOException e){
            log.info("有些瓦片下载失败，将瓦片信息写入文件时发生错误，请检查！");
        }
        // 给个退出信号
        isDownloading = false;
    }

    @PostConstruct
    private void input() throws Exception {
        System.out.println("请认真核实以下参数：");
        ((CommandLineRunner) args -> {
            Scanner scanner = new Scanner(System.in);
            int step = 1;
            System.out.println("1. 确认左下角坐标经纬度: " + bbox[0] + ", " + bbox[1] + " , 如需修改直接输入，不修改直接回车键继续");
            while (step > 0){
                String in = scanner.nextLine();
                if (!"".equals(in)) {
                    String[] sts = in.split(",");
                    bbox[0] = Double.valueOf(sts[0]);
                    bbox[1] = Double.valueOf(sts[1]);
                }
                System.out.println(++step + ". 确认右上角坐标经纬度： " + bbox[2] + ", " + bbox[3] + ", 如需修改直接输入，不修改直接回车键继续");
                in = scanner.nextLine();
                if (!"".equals(in)) {
                    String[] sts = in.split(",");
                    bbox[2] = Double.valueOf(sts[0]);
                    bbox[3] = Double.valueOf(sts[1]);
                }
                System.out.println(++step + ". 确认下载的瓦片最小级别： " + minLevel + ", 如需修改直接输入，不修改直接回车键继续");
                in = scanner.nextLine();
                if (!"".equals(in)) {
                    minLevel = Integer.valueOf(in);
                }
                System.out.println(++step + ". 确认下载的瓦片最大级别： " + maxLevel + ", 如需修改直接输入，不修改直接回车键继续");
                in = scanner.nextLine();
                if (!"".equals(in)) {
                    maxLevel = Integer.valueOf(in);
                }
                System.out.println(++step + ". 确认瓦片保存的路径： " + pngDir + ", 如需修改直接输入，不修改直接回车键继续");
                in = scanner.nextLine();
                if (!"".equals(in)) {
                    pngDir = in;
                }
                step = -1;
            }
            begin();
        }).run("");
    }
}
